Logo
Published on

4.1.FastAPI 通用模块

Authors
  • avatar
    Name
    xiaobai
    Twitter

1.概念介绍

通用模块,这里指的就是之前在课件中搭建部署的前后端系统中所使用的通用模块;通用模块的核心特点是:

  1. 基于动态SQL拼接和参数化查询技术实现标准化CRUD操作,采用与语言无关的设计架构,可无缝移植到Node.js、Go、Python、Rust等主流后端环境;
  2. 弱类型约束,这个特性既是优点也是缺点;缺点就是在使用通用模块进行增删改查时得到的数据,以及调用参数都不会有参数类型提示以及约束,优点就是可以支持通过配置热加载新模块接口,无需服务重启即可扩展数据操作能力;
  3. 容易与dify工作流集成,或者整合到LangChain、LangGraph等智能应用开发体系中,使得AI模型应用具备通用的数据库增删改查能力;
  4. 对于快速原型开发来说,通用模块的使用场景非常广泛,很多时候为了能够为客户或者团队展示原型功能界面,需要快速生成对应的接口,那么通用模块在这种场景下就是十分必要的功能;

后续我们的dify实战项目中,会实战手写开发一个dify工具,这个工具的功能就是实现这个通用模块的功能,实现在dify工作流中只需要简单地配置就能够对所有数据库表进行通用的增删改查功能;

2.参数说明

有关通用模块增删改查接口的参数,请参考课件“3.1.后端开发”中的第4节“通用接口参数”;

3.通用模块在Nest与FastAPI中的实现区别

之前在“3.1.后端开发”章节中讲过在Node中,这个通用模块的实现原理,这里在FastAPI中,实现原理是差不多的,主要区别有以下几点:

  1. 接口定义方式:
    1. Nest中通用模块的接口是通过动态路由实现的,增删改查各自对应一个路由接口,也就是Controller类的成员方法;
    2. FastAPI中也是通过动态路由实现的,但是增删改查接口是使用数组批量生成的;
  2. 执行SQL方式:
    1. Nest中是通过调用typeorm的dataSource来手动执行sql;
    2. FastAPI中是通过调用sqlmodel的AsyncSession.connection获取连接来手动执行sql;
  3. 日期格式化字符串:
    1. Nest中日期格式化字符串为“%Y-%m-%d %H:%i:%s”
    2. FastAPI中日期格式化字符串为“%%Y-%%m-%%d %%H:%%i:%%s”
    3. 因为Nest中使用的占位符为“?”,FastAPI中使用的占位符就是“%s”,以至于FastAPI中需要对百分号做转义;

4.FastAPI中手动执行SQL

  • 这里我们继续基于之前创建的langserve项目来讲解通用模块的实现过程;
  • 首先我们之前就已经在使用sqlmodel的面向对象的方式来操作数据库,那么这里sqlmodel的定位类似于我们后端Nest工程中的typeorm,我们同样可以使用sqlmodel提供的方法来手动执行sql语句;
  • 如下所示,是一个使用sqlmodel来实现参数化查询的一个示例代码,这里需要特别注意的是,参数化查询的参数得是一个元组,如果用数组来管理参数,那么在传入的时候就需要使用tuple函数来将数组转化为元组;
@app.get("/query_llm_user")
async def query_llm_user(username: str, session: AsyncSessionDep):
  result = await (await session.connection()).exec_driver_sql(
    "select * from llm_user where username = %s",
    tuple([username])
  )
  list = [dict(row._mapping) for row in result]
  return {"result": list[0]} if len(list) == 1 else {"result": None}

5.通用模块实现

5.1.准备工具函数 GeneralUtils

新增文件“app/general/sql_utils/GeneralUtils.py”,代码如下所示,这个“GeneralUtils”类中所有函数都是静态函数:

  1. log_sql:负责打印sql,会自动将参数填充到sql中打印到控制台;
  2. get_value_sql:负责生成字段的取值sql,如果字段是字符串或者数字,那么就是占位符“?”,如果是日期时间类的字段,则生成的sql为使用数据库函数将字符串转化为数据库日期时间值;
  3. get_value:一个普通的用来从字典或者对象中取某个属性值的函数;
  4. format_columns:根据字段信息生成两个字典,一个是通过驼峰命名找到字段对象的字典映射对象,另一个是通过下划线命名找到字段对象的字典映射对象;
  5. to_line:驼峰命名转化成下划线命名;
import os
import re

class GeneralUtils:

  @staticmethod
  def log_sql(sql, values):
    """打印sql执行日志,会将sql中的?占位符替换为对应顺序的参数值"""

    show_sql = os.getenv('GENERAL_SHOW_SQL', True)

    if show_sql:
      print("\n/*---------------------------------------log sql-------------------------------------------*/\n")
      print("-" * 20, "原始SQL", "-" * 20)
      print(sql)
      print("-" * 20, "SQL参数", "-" * 20)
      print(values)
      count = 0

      def replace_callback(match):
        nonlocal count
        val = values[count]
        count = count + 1
        if isinstance(val, str):
          return val
        if isinstance(val, list):
          return ', '.join(map(str, val))
        return str(val)

      target_sql = re.sub(r'\?+', replace_callback, sql)
      print("-" * 20, "目标SQL", "-" * 20)
      print(target_sql)
      print("\n")

  @staticmethod
  def get_value_sql(value, value_type, sql_values):
    """获取值sql,如果是字符串或者数字则直接返回占位符?,如果是日期时间则返回转化值占位字符串?"""
    if value_type == 'string' or value_type == 'number':
      sql_values.append(value)
      return '?'
    elif value_type == 'date':
      sql_values.append(value)
      return "str_to_date(?, '%%Y-%%m-%%d')"
    elif value_type == 'datetime':
      sql_values.append(value)
      return "str_to_date(?, '%%Y-%%m-%%d %%H:%%i:%%s')"
    elif value_type == 'time':
      sql_values.append(value)
      return "str_to_date(?, '%%H:%%i:%%s')"

  @staticmethod
  def get_value(obj, attr_name, default=None):
    """通用的获取属性值的方法"""
    if isinstance(obj, dict):
      return obj.get(attr_name, default)
    else:
      return getattr(obj, attr_name, default)

  @staticmethod
  def format_columns(columns):
    """
    根据字段信息生成两个字典,
    一个是通过驼峰命名找到字段对象的字典映射对象,
    另一个是通过下划线命名找到字段对象的字典映射对象
    """

    hump_to_columns = {}
    line_to_columns = {}

    for hump_name, col_config in columns.items():
      line_name = GeneralUtils.to_line(hump_name)
      query = GeneralUtils.get_value(col_config, "query", None) or f"t1.{line_name}"
      info = {
        **col_config,
        "hump_name": hump_name,
        "line_name": line_name,
        "query": query,
        "col_name": query.split('.')[1]
      }
      hump_to_columns[hump_name] = info
      line_to_columns[line_name] = info
    return {
      # 通过下划线命令查找列信息
      "hump_to_columns": hump_to_columns,
      # 通过驼峰命名查找列信息
      "line_to_columns": line_to_columns,
    }

  @staticmethod
  def to_line(hump_name: str) -> str:
    """驼峰命名转化成下划线命名"""
    return re.sub(r'([A-Z])', r'_\1', hump_name).lower()

5.2.准备转化器类 GeneralConvertor

  • 新增文件“app/general/sql_utils/GeneralConvertor.py”;
  • 转化器类“GeneralConvertor”主要做两个事情:
    • 遍历前端通过接口参数传入的数据对象的字段,如果字段有配置转化设置,则将字段值转化为字符串存储在数据库中;
    • 将查出来的数据对象,遍历数据对象的字段,如果字段有配置转化设置,则将字段值从字符串解析为数组;
  • 第一个处理操作,将前端传入的数据对象的字段值转化为字符串保存,会发生在新建、更新、批量新建、批量更新中;
  • 第二个处理操作,将查出来的数据对象的字段值从字符串转化为数组,会发生在分页查询、单条查询、新建、批量新建、更新、批量更新等操作中;
  • 转化器主要有两种转化方式,一种是前端传入、以及返回给前端的值是任意类型值数组,但是数据库中存储的是json数组字符串;第二种是前端传入以及返回的值是字符串数组,但是数据库中存储的是逗号分隔的字符串;

代码如下所示:

import json

from app.general.sql_utils.GeneralUtils import GeneralUtils

class GeneralConvertor:
  def __init__(self, module_config):
    self.convert_columns = [
      (col_name, col_config)
      for col_name, col_config in module_config["columns"].items()
      if col_config.get("convert")
    ]

  def encode_list(self, arr):
    if not len(self.convert_columns):
      return
    for item in arr:
      for col_name, col_config in self.convert_columns:
        if GeneralUtils.get_value(item, col_name, None) is not None:
          if not isinstance(item[col_name], str):
            item[col_name] = ConvertTypes[col_config["convert"]]["encode"](item[col_name])

  def decode_list(self, arr):
    if not len(self.convert_columns):
      return
    for item in arr:
      for col_name, col_config in self.convert_columns:
        if GeneralUtils.get_value(item, col_name, None) is not None:
          if isinstance(item[col_name], str):
            item[col_name] = ConvertTypes[col_config["convert"]]["decode"](item[col_name])


MODULE_CONVERT_TYPE_ARRAY_JSON = 'arrayjson'
MODULE_CONVERT_TYPE_ARRAY_STRING = 'arraystring'


def array_json_encoder(val):
  return json.dumps(val)


def array_json_decoder(val):
  try:
    return json.loads(val)
  except json.JSONDecodeError:
    return ""


def array_string_encoder(val):
  return ",".join(val) if isinstance(val, list) else val


def array_string_decoder(val):
  return val.split(",") if isinstance(val, str) else val


ConvertTypes = {
  "arrayjson": {
    "encode": array_json_encoder,
    "decode": array_json_decoder,
  },
  "arraystring": {
    "encode": array_string_encoder,
    "decode": array_string_decoder,
  }
}

如下所示比如在模块的字段配置中,两个字段各自配置了“convert”:

"arrayJson": {
  "valueType": "string",
  "convert": "arrayjson",
  "front": {
    "title": "Json数组",
    "type": "string"
  }
},
"arrayString": {
  "valueType": "string",
  "convert": "arraystring",
  "front": {
    "title": "String数组",
    "type": "string"
  }
},
  • 那么后端返回的数据中,这两个字段就会自动转化为数组对象,如下所示;
  • 同样的前端在将数据调用后端接口时,也是以对象的方式传递;

img

但是存储在数据库中,这个字段值的类型还是字符串;

img

5.3.生成 INSERT SQL

新建文件“app/general/sql_utils/build_insert_sql.py”:

from app.general.sql_utils.GeneralUtils import GeneralUtils


def build_insert_sql(module_config, row):
  column_info = GeneralUtils.format_columns(module_config["columns"])
  sqls = [f"insert into {GeneralUtils.get_value(module_config, 'tableName')}"]
  values = []

  field_sql_left_list = []
  field_sql_right_list = []
  field_sql_right_values = []

  for hump_name, column in column_info['hump_to_columns'].items():
    value = GeneralUtils.get_value(row, hump_name)

    # if (value === undefined) {return;}

    if value is None:
      continue;

    if 't1.' not in column['query']:
      continue

    field_sql_left_list.append(column['col_name'])
    field_sql_right_list.append(GeneralUtils.get_value_sql(
      value=value,
      value_type=column['valueType'],
      sql_values=field_sql_right_values,
    ))

  sqls.append(f"( {', '.join(field_sql_left_list)} ) ")
  sqls.append("values")
  sqls.append(f"( {', '.join(field_sql_right_list)} )")
  values.extend(field_sql_right_values)
  sql = ' '.join(sqls)
  GeneralUtils.log_sql(sql, values)
  sql = sql.replace("?", "%s")
  return (sql, values)


# build_insert_sql(DEMO_MODULE_CONFIG, {"normalText": "123", "numberVal": 213})

生成的sql示例:

-------------------- 原始SQL --------------------
insert into pl_demo ( normal_text, number_val )  values ( ?, ? )
-------------------- SQL参数 --------------------
['123', 213]
-------------------- 目标SQL --------------------
insert into pl_demo ( normal_text, number_val )  values ( 123, 213 )

5.4.生成 UPDATE SQL

新建文件“app/general/sql_utils/build_update_sql.py”

from datetime import datetime

from app.general.sql_utils.GeneralUtils import GeneralUtils

UPDATE_EXCLUDE_FIELDS = ['id', 'createAt', 'createdBy']

def build_update_sql(module_config, row, param_update_fields=None):
  row_id = GeneralUtils.get_value(row, 'id', None)

  if row_id is None:
    raise Exception("row_id is None")

  column_info = GeneralUtils.format_columns(module_config["columns"])
  sqls = [f"update {module_config['tableName']} set"]
  values = []

  field_sql_list = []
  for hump_name, column in column_info['hump_to_columns'].items():
    value = GeneralUtils.get_value(row, hump_name, None)
    if hump_name in UPDATE_EXCLUDE_FIELDS:
      continue
    if 't1.' not in column['query']:
      continue
    if param_update_fields is not None and hump_name not in param_update_fields:
      # 如果有指定更新的字段,并且humpName不在这个字段列表中,则不更新这个字段
      continue
    if hump_name == 'updateAt':
      value = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    field_sql_list.append(f"""{column['col_name']} = {GeneralUtils.get_value_sql(
      value=value,
      value_type=column['valueType'],
      sql_values=values,
    )}""")
  sqls.append(', '.join(field_sql_list))
  sqls.append("where id = ?")
  values.append(row_id)

  sql = ' '.join(sqls)
  GeneralUtils.log_sql(sql, values)
  sql = sql.replace("?", "%s")
  return (sql, values)


# build_update_sql(DEMO_MODULE_CONFIG, DEMO_QUERY_ROWS[0], ['normalText', 'numberVal'])

生成的sql示例:

-------------------- 原始SQL --------------------
update pl_demo set normal_text = ?, number_val = ? where id = ?
-------------------- SQL参数 --------------------
['hello', 602, '8570453e-0c88-11ef-9e34-5254001d6dcd']
-------------------- 目标SQL --------------------
update pl_demo set normal_text = hello, number_val = 602 where id = 8570453e-0c88-11ef-9e34-5254001d6dcd

5.5.生成 DELETE SQL

from app.general.sql_utils.GeneralUtils import GeneralUtils


def build_delete_sql(module_config, id):
  if isinstance(id, list):
    sql = f"delete from {module_config['tableName']} where id in ({','.join(['?'] * len(id))})"
    values = id

  else:
    sql = f"delete from {module_config['tableName']} where id = ?"
    values = [id]

  GeneralUtils.log_sql(sql, values)
  sql = sql.replace("?", "%s")
  return (sql, values)


# sql, values = build_delete_sql(DEMO_MODULE_CONFIG, ['111', '2222'])

生成的sql示例:

-------------------- 原始SQL --------------------
delete from pl_demo where id in (?,?)
-------------------- SQL参数 --------------------
['111', '2222']
-------------------- 目标SQL --------------------
delete from pl_demo where id in (111,2222)

5.6.生成 QUERY SQL

import re

from app.general.sql_utils.GeneralUtils import GeneralUtils


def build_query_sql(query_config, module_config):
  column_info = GeneralUtils.format_columns(module_config["columns"])

  # print("column_info ==>>", column_info)

  distinct_fields = GeneralUtils.get_value(query_config, 'distinctFields', [])

  has_distinct = len(distinct_fields) > 0

  # /*---------------------------------------field sql-------------------------------------------*/
  field_sql_list = []
  field_sql_values = []

  if has_distinct:
    print("has_distinct", distinct_fields)
    field_sql_list.append("distinct")
    distinct_field_strings = []
    for item_distinct_field in distinct_fields:
      item_column_info = GeneralUtils.get_value(GeneralUtils.get_value(column_info, 'humpToColumns'), item_distinct_field)
      item_field_string = f"{query_format(item_column_info['query'], item_column_info['valueType'])}"

      if not GeneralUtils.get_value(query_config, 'onlyCount', False):
        item_field_string = f"{item_field_string} as '{item_column_info['hump_name']}'"
      distinct_field_strings.append(item_field_string)
    field_sql_list.append(','.join(distinct_field_strings))
  else:
    field_strings = []
    for hump_name, _ in module_config['columns'].items():
      item_column_info = column_info["hump_to_columns"][hump_name]
      field_strings.append(f"{query_format(item_column_info['query'], item_column_info['valueType'])} as '{item_column_info['hump_name']}'")
    field_sql_list.append(','.join(field_strings))

  # print("field_sql_list ==>>", field_sql_list)
  # print("field_sql_values ==>>", field_sql_values)

  # /*---------------------------------------from sql-------------------------------------------*/

  from_sql_list = ['from']
  from_sql_values = []

  from_sql_list.append(f"{module_config['tableName']} t1")

  join_config = GeneralUtils.get_value(module_config, 'joinConfig', [])
  if len(join_config):
    for item_join_config in join_config:
      item_join_config_type = GeneralUtils.get_value(item_join_config, 'type', '')
      if item_join_config_type != 'right join' and item_join_config_type != 'left join' and item_join_config_type != 'join':
        raise ValueError(f"Can't recognise join type:{item_join_config_type}")
      on_left, on_right = item_join_config['on'].split('=')
      left_table, left_field = on_left.strip().split('.')
      right_table, right_field = on_right.strip().split('.')
      from_sql_list.append(f"{item_join_config_type} {item_join_config['table']} {item_join_config['alia']} on {left_table}.{left_field} = {right_table}.{right_field}")

  # /*---------------------------------------filter sql-------------------------------------------*/

  filter_sql_list = []
  filter_sql_values = []

  query_config_filters = GeneralUtils.get_value(query_config, 'filters', [])

  if len(query_config_filters):
    for index, item_filter in enumerate(query_config_filters):
      # print(index, item_filter)
      if GeneralUtils.get_value(item_filter, 'id', None) is None:
        item_filter['id'] = f"_{index}"
    # print(query_config_filters)
    filter_expression = GeneralUtils.get_value(query_config, 'filterExpression', None) or ' and '.join(item['id'] for item in query_config_filters)
    filter_expression = re.sub(r'\s+(并且|&&)\s+', ' and ', filter_expression)
    filter_expression = re.sub(r'\s+(或者|\|\|)\s+', ' or ', filter_expression)

    id_2_filter = {item['id']: item for item in query_config_filters}

    def replace_func(match):

      full_match = match.group(0)
      filter_id = full_match

      if filter_id == 'and' or filter_id == 'or':
        return filter_id

      filter_info = GeneralUtils.get_value(id_2_filter, filter_id, None)

      if filter_info is None:
        return f"[NoMatchFilterForId:{filter_id}]"

      filter_field = filter_info['field']
      item_column = GeneralUtils.get_value(column_info['hump_to_columns'], filter_field, None)

      if item_column is None:
        return f"[NoMatchColumnForField:{filter_field}]"

      filter_type = GeneralUtils.get_value(filter_info, 'type', None) or GeneralUtils.get_value(item_column, 'valueType', None) or 'string'
      value = GeneralUtils.get_value(filter_info, 'value', None)
      filter_operator = filter_info['operator']
      query = item_column['query']

      if filter_type == "string":
        if filter_operator == '=' or filter_operator == '>' or filter_operator == '>=' or filter_operator == '<' or filter_operator == '<=':
          filter_sql_values.append(value)
          return f"{query} = ?"
        elif filter_operator == '!=':
          filter_sql_values.append(value)
          return f"{query} != ?"
        elif filter_operator == '~':
          filter_sql_values.append(f"%{value}%")
          return f"{query} like ?"
        elif filter_operator == 'in':
          return format_in(value, query, False, filter_sql_values)
        elif filter_operator == 'not in':
          return format_in(value, query, True, filter_sql_values)
        elif filter_operator == 'in like':
          return format_in_like(value, query, False, filter_sql_values)
        elif filter_operator == 'not in like':
          return format_in_like(value, query, True, filter_sql_values)
        elif filter_operator == 'is null':
          return f"{query} is null"
        elif filter_operator == 'is not null':
          return f"{query} is not null"
      elif filter_type == "number":
        if filter_operator == '=':
          filter_sql_values.append(value)
          return f"{query} = ?"
        elif filter_operator == '!=':
          filter_sql_values.append(value)
          return f"{query} != ?"
        elif filter_operator == '~':
          filter_sql_values.append(f"%{value}%")
          return f"{query} like ?"
        elif filter_operator == '>':
          filter_sql_values.append(value)
          return f"{query} > ?"
        elif filter_operator == '>=':
          filter_sql_values.append(value)
          return f"{query} >= ?"
        elif filter_operator == '<':
          filter_sql_values.append(value)
          return f"{query} < ?"
        elif filter_operator == '<=':
          filter_sql_values.append(value)
          return f"{query} <= ?"
        elif filter_operator == 'in':
          return format_in(value, query, False, filter_sql_values)
        elif filter_operator == 'not in':
          return format_in(value, query, True, filter_sql_values)
        elif filter_operator == 'in like':
          return format_in_like(value, query, False, filter_sql_values)
        elif filter_operator == 'not in like':
          return format_in_like(value, query, True, filter_sql_values)
        elif filter_operator == 'is null':
          return f"{query} is null"
        elif filter_operator == 'is not null':
          return f"{query} is not null"
      elif filter_type == "date":
        if filter_operator == '=' or filter_operator == '~':
          filter_sql_values.append(value)
          return f"{date_format_sql(query)} = ?"
        elif filter_operator == '!=':
          filter_sql_values.append(value)
          return f"{date_format_sql(query)} != ?"
        elif filter_operator == '>':
          filter_sql_values.append(value)
          return f"{query} > ?"
        elif filter_operator == '>=':
          filter_sql_values.append(value)
          return f"{query} >= ?"
        elif filter_operator == '<':
          filter_sql_values.append(value)
          return f"{query} < ?"
        elif filter_operator == '<=':
          filter_sql_values.append(value)
          return f"{query} <= ?"
        elif filter_operator == 'in' or filter_operator == 'in like':
          v_list = format_string2array(value)
          filter_sql_values.extend(v_list)
          return f"{date_format_sql(query)} in ({','.join('?' for _ in v_list)})"
        elif filter_operator == 'not in' or filter_operator == 'not in like':
          v_list = format_string2array(value)
          filter_sql_values.extend(v_list)
          return f"{date_format_sql(query)} not in ({','.join('?' for _ in v_list)})"
        elif filter_operator == 'is null':
          return f"{query} is null"
        elif filter_operator == 'is not null':
          return f"{query} is not null"
      elif filter_type == "time":
        if filter_operator == '=' or filter_operator == '~':
          filter_sql_values.append(value)
          return f"{time_format_sql(query)} = ?"
        elif filter_operator == '!=':
          filter_sql_values.append(value)
          return f"{time_format_sql(query)} != ?"
        elif filter_operator == '>':
          filter_sql_values.append(value)
          return f"{query} > ?"
        elif filter_operator == '>=':
          filter_sql_values.append(value)
          return f"{query} >= ?"
        elif filter_operator == '<':
          filter_sql_values.append(value)
          return f"{query} < ?"
        elif filter_operator == '<=':
          filter_sql_values.append(value)
          return f"{query} <= ?"
        elif filter_operator == 'in' or filter_operator == 'in like':
          v_list = format_string2array(value)
          filter_sql_values.extend(v_list)
          return f"{time_format_sql(query)} in ({','.join('?' for _ in v_list)})"
        elif filter_operator == 'not in' or filter_operator == 'not in like':
          v_list = format_string2array(value)
          filter_sql_values.extend(v_list)
          return f"{time_format_sql(query)} not in ({','.join('?' for _ in v_list)})"
        elif filter_operator == 'is null':
          return f"{query} is null"
        elif filter_operator == 'is not null':
          return f"{query} is not null"
        return
      elif filter_type == "datetime":
        if filter_operator == '=' or filter_operator == '~':
          filter_sql_values.append(value)
          return f"{datetime_format_sql(query)} = ?"
        elif filter_operator == '!=':
          filter_sql_values.append(value)
          return f"{datetime_format_sql(query)} != ?"
        elif filter_operator == '>':
          filter_sql_values.append(value)
          return f"{query} > ?"
        elif filter_operator == '>=':
          filter_sql_values.append(value)
          return f"{query} >= ?"
        elif filter_operator == '<':
          filter_sql_values.append(value)
          return f"{query} < ?"
        elif filter_operator == '<=':
          filter_sql_values.append(value)
          return f"{query} <= ?"
        elif filter_operator == 'not in' or filter_operator == 'not in like':
          v_list = format_string2array(value)
          filter_sql_values.extend(v_list)
          return f"{datetime_format_sql(query)} in ({','.join('?' for _ in v_list)})"
        elif filter_operator == 'in' or filter_operator == 'in like':
          v_list = format_string2array(value)
          filter_sql_values.extend(v_list)
          return f"{datetime_format_sql(query)} not in ({','.join('?' for _ in v_list)})"
        elif filter_operator == 'is null':
          return f"{query} is null"
        elif filter_operator == 'is not null':
          return f"{query} is not null"
        return
      else:
        return f"NoMatchFilterType:{filter_type}"

      return f"filter type {filter_type} no match operator: ${filter_operator}"

    new_filter_expression = re.sub(r'[a-zA-Z0-9_-]+', replace_func, filter_expression)

    filter_sql_list.extend(['where', new_filter_expression])

  # /*---------------------------------------only count-------------------------------------------*/

  sqls = []
  values = []

  query_config_only_count = GeneralUtils.get_value(query_config, 'onlyCount', False)

  if query_config_only_count:
    if not has_distinct:
      sqls.append("select count(0) as total")
    else:
      sqls.append(f"select count( {' '.join(field_sql_list)} ) as total")
      values.extend(field_sql_values)

    sqls.extend(from_sql_list)
    values.extend(from_sql_values)

    sqls.extend(filter_sql_list)
    values.extend(filter_sql_values)
  else:
    sqls.append('select')

    sqls.extend(field_sql_list)
    values.extend(field_sql_values)

    sqls.extend(from_sql_list)
    values.extend(from_sql_values)

    sqls.extend(filter_sql_list)
    values.extend(filter_sql_values)

    def get_sort_sql_value():
      sort_sql_list = []
      sort_sql_values = []

      query_config_orders = GeneralUtils.get_value(query_config, 'orders', [])
      query_config_orders = query_config_orders if isinstance(query_config_orders, list) else [query_config_orders]

      if len(query_config_orders) > 0:
        sort_sql_list.append("order by")
        temp_list = []

        for sort_item in query_config_orders:
          sn = ''
          sc = ''

          if isinstance(sort_item, str):
            sn = sort_item
            sc = 'desc'
          else:
            sn = sort_item['field']
            sc = 'desc' if sort_item['desc'] else 'asc'

          column_item = GeneralUtils.get_value(column_info['hump_to_columns'], sn, None)

          if column_item is None:
            temp_list.append(f"[NoMatchSortField:{sn}]")
          else:
            temp_list.append(f"{column_item['query']} {sc}")

        sort_sql_list.append(', '.join(temp_list))
      return (sort_sql_list, sort_sql_values)

    sort_sql_list, sort_sql_values = get_sort_sql_value()

    sqls.extend(sort_sql_list)
    values.extend(sort_sql_values)

  query_config_all = GeneralUtils.get_value(query_config, 'all', False)

  if not query_config_all:
    sqls.append("limit ?,?")
    values.extend([
      GeneralUtils.get_value(query_config, 'offset', 0),
      GeneralUtils.get_value(query_config, 'size', 10)
    ])
  # /*---------------------------------------end-------------------------------------------*/

  sql = ' '.join(sqls)
  GeneralUtils.log_sql(sql, values)
  sql = sql.replace("?", "%s")

  return (sql, values)


def query_format(query: str, value_type: str):
  if value_type == "string" or value_type == "number":
    return query
  elif value_type == "date":
    return date_format_sql(query)
  elif value_type == "datetime":
    return datetime_format_sql(query)
  elif value_type == "time":
    return time_format_sql(query)


def date_format_sql(query: str):
  return f"date_format({query}, '%%Y-%%m-%%d')"


def datetime_format_sql(query: str):
  return f"date_format({query}, '%%Y-%%m-%%d %%H:%%i:%%s')"


def time_format_sql(query: str):
  return f"date_format({query}, '%%H:%%i:%%s')"


def format_in(value, query, not_in, value_list):
  # value_list.append(query)
  # 如果 value 不是列表,将其按逗号分割成列表
  if not isinstance(value, list):
    list_ = value.split(',')
  else:
    list_ = value

  # 将 list_ 中的元素添加到 valueList 中
  value_list.extend(list_)

  # 生成格式化字符串
  result = f"{query} {'not ' if not_in else ''}in ({','.join('?' for _ in list_)})"
  return result


def format_in_like(value, query, not_like, value_list):
  # 如果 value 不是列表,将其按逗号分割成列表
  if not isinstance(value, list):
    list_ = value.split(',')
  else:
    list_ = value

  if not_like:
    result_list = []
    for item in list_:
      value_list.append(f"%{item}%")
      result_list.append(f"{query} not like ?")
    return f"({' and '.join(result_list)})"
  else:
    result_list = []
    for item in list_:
      value_list.append(f"%{item}%")
      result_list.append(f"{query} like ?")
    return f"({' or '.join(result_list)})"


def format_string2array(val):
  return val if isinstance(val, list) else val.split(',')


# /*---------------------------------------查询sql生成测试-------------------------------------------*/

# sql, values = build_query_sql({
#   "page": 0,
#   "size": 10,
#   "filters": [
#     {
#       "field": "count",
#       "operator": "is null",
#       "id": "query_meta_1"
#     }
#   ]
# }, DEMO_MODULE_CONFIG)

生成的sql示例:

-------------------- 原始SQL --------------------
select t1.id as 'id',date_format(t1.created_at, '%%Y-%%m-%%d %%H:%%i:%%s') as 'createdAt',t1.created_by as 'createdBy',date_format(t1.updated_at, '%%Y-%%m-%%d %%H:%%i:%%s') as 'updatedAt',t1.updated_by as 'updatedBy',t1.count as 'count',t1.normal_text as 'normalText',t1.long_text as 'longText',t1.number_val as 'numberVal',t1.flag as 'flag',t1.select_val as 'selectVal',t1.color_val as 'colorVal',date_format(t1.date_val, '%%Y-%%m-%%d') as 'dateVal',date_format(t1.time_val, '%%H:%%i:%%s') as 'timeVal',t1.parent_id as 'parentId',t1.image_id as 'imageId',t2.normal_text as 'parentName',t1.province_val as 'provinceVal',t1.city_val as 'cityVal',t1.district_val as 'districtVal',t1.ov_val as 'ovVal',t1.array_json as 'arrayJson',t1.array_string as 'arrayString',t1.array_json_str as 'arrayJsonStr',t1.array_string_str as 'arrayStringStr' from pl_demo t1 left join pl_demo t2 on t1.parent_id = t2.id where t1.count is null limit ?,?
-------------------- SQL参数 --------------------
[0, 10]
-------------------- 目标SQL --------------------
select t1.id as 'id',date_format(t1.created_at, '%%Y-%%m-%%d %%H:%%i:%%s') as 'createdAt',t1.created_by as 'createdBy',date_format(t1.updated_at, '%%Y-%%m-%%d %%H:%%i:%%s') as 'updatedAt',t1.updated_by as 'updatedBy',t1.count as 'count',t1.normal_text as 'normalText',t1.long_text as 'longText',t1.number_val as 'numberVal',t1.flag as 'flag',t1.select_val as 'selectVal',t1.color_val as 'colorVal',date_format(t1.date_val, '%%Y-%%m-%%d') as 'dateVal',date_format(t1.time_val, '%%H:%%i:%%s') as 'timeVal',t1.parent_id as 'parentId',t1.image_id as 'imageId',t2.normal_text as 'parentName',t1.province_val as 'provinceVal',t1.city_val as 'cityVal',t1.district_val as 'districtVal',t1.ov_val as 'ovVal',t1.array_json as 'arrayJson',t1.array_string as 'arrayString',t1.array_json_str as 'arrayJsonStr',t1.array_string_str as 'arrayStringStr' from pl_demo t1 left join pl_demo t2 on t1.parent_id = t2.id where t1.count is null limit 0,10

5.7.SQL执行类 GeneralService

上面的几个函数只是用来根据模块配置信息module_config以及接口参数生成需要执行的sql以及占位参数,这里即将实现的“GeneralService”才是用来执行数据库操作的主体:

from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncConnection

from app.general.sql_utils.GeneralConvertor import GeneralConvertor
from app.general.sql_utils.GeneralUtils import GeneralUtils
from app.general.sql_utils.build_delete_sql import build_delete_sql
from app.general.sql_utils.build_insert_sql import build_insert_sql
from app.general.sql_utils.build_query_sql import build_query_sql
from app.general.sql_utils.build_update_sql import build_update_sql
from app.utils.next_id import next_id


class GeneralService:
  def __init__(self, module_config):
    self.module_config = module_config
    self.convertor = GeneralConvertor(module_config)

  async def select(self, conn: AsyncConnection, query_config, debug_data=[]):

    n_page = GeneralUtils.get_value(query_config, 'page', 0)
    n_size = GeneralUtils.get_value(query_config, 'size', 5)
    n_only_count = GeneralUtils.get_value(query_config, 'onlyCount', False)

    offset = n_page * n_size
    # 多查一条数据,方便判断是否有下一页数据
    size = n_size + 1

    target_query_config = {
      **query_config,
      "offset": offset,
      "size": size,
      "orders": get_default_orders(query_config, self.module_config)
    }

    sql, values = build_query_sql(target_query_config, self.module_config)

    try:

      debug_data.append({"sql": sql, "values": values})
      result = await conn.exec_driver_sql(sql, tuple(values))
      result = [dict(row._mapping) for row in result]

      self.convertor.decode_list(result)

      if n_only_count:
        return {"total": result[0]['total']}
      else:
        has_next = False if GeneralUtils.get_value(query_config, 'all', False) else len(result) == n_size + 1
        if has_next:
          result.pop()
        return {
          "hasNext": has_next,
          "list": result,
        }
    except SQLAlchemyError as err:
      return {"error": f"Error: {err}", }

  async def select_one(self, conn: AsyncConnection, query_config, debug_data=[]):
    target_query_config = {
      "offset": 0,
      "size": 1,
      "filters": [],
      "orders": {"field": "createdAt", "desc": True},
    }
    for humpName, value in query_config.items():
      target_query_config['filters'].append({
        "field": humpName,
        "value": value,
        "operator": "="
      })

    result = await self.select(conn=conn, query_config=target_query_config, debug_data=debug_data)

    if "error" in result:
      return result

    return {"result": None if "list" not in result or len(result['list']) == 0 else result['list'][0]}

  async def insert(self, conn: AsyncConnection, query_config, debug_data=[]):

    row = GeneralUtils.get_value(query_config, 'row', None)
    if row is None:
      return {
        "error": "row parameter is missing",
      }
    self.convertor.encode_list([row])

    row_id = GeneralUtils.get_value(row, 'id', None)

    if row_id is None:
      row_id = await next_id(1)
      row['id'] = row_id

    try:
      sql, values = build_insert_sql(self.module_config, row)

      debug_data.append({"sql": sql, "values": values})
      await conn.exec_driver_sql(sql, tuple(values))
      await conn.commit()

      sql, values = build_query_sql({"page": 0, "size": 1, "filters": [{"field": "id", "operator": "=", "value": row_id}]}, self.module_config)
      debug_data.append({"sql": sql, "values": values})
      result = await conn.exec_driver_sql(sql, tuple(values))
      result = [dict(row._mapping) for row in result]

      self.convertor.decode_list(result)

      if len(result) > 0:
        return {"result": result[0]}
      else:
        return {
          "error": "insert failed, query result is empty",
        }

    except SQLAlchemyError as err:
      return {"error": f"Error: {err}", }

  async def batch_insert(self, conn: AsyncConnection, query_config, debug_data=[]):

    rows = GeneralUtils.get_value(query_config, 'rows', None)
    if rows is None or len(rows) == 0:
      return {"error": "rows parameter is missing", }
    self.convertor.encode_list(rows)

    row_id_list = []
    for row in rows:
      row_id = GeneralUtils.get_value(row, 'id', None)
      if row_id is None:
        row_id = await next_id(1)
        row['id'] = row_id
      row_id_list.append(row_id)

    try:
      for row in rows:
        sql, values = build_insert_sql(self.module_config, row)
        debug_data.append({"sql": sql, "values": values})
        await conn.exec_driver_sql(sql, tuple(values))

      await conn.commit()

      sql, values = build_query_sql({"all": True, "filters": [{"field": "id", "operator": "in", "value": row_id_list}]}, self.module_config)
      debug_data.append({"sql": sql, "values": values})
      result = await conn.exec_driver_sql(sql, tuple(values))
      result = [dict(row._mapping) for row in result]

      self.convertor.decode_list(result)

      if len(result) > 0:
        return {"result": result}
      else:
        return {"error": "insert failed, query result is empty", }

    except SQLAlchemyError as err:
      return {"error": f"Error: {err}", }

  async def update(self, conn: AsyncConnection, query_config, debug_data=[]):

    row = GeneralUtils.get_value(query_config, 'row', None)
    update_fields = GeneralUtils.get_value(query_config, 'updateFields', None)

    if row is None:
      return {"error": "row parameter is missing", }

    row_id = GeneralUtils.get_value(row, 'id', None)

    if row_id is None:
      return {"error": "row is missing field: id", }

    self.convertor.encode_list([row])

    try:
      sql, values = build_update_sql(self.module_config, row, update_fields)

      debug_data.append({"sql": sql, "values": values})
      await conn.exec_driver_sql(sql, tuple(values))
      await conn.commit()

      sql, values = build_query_sql({"page": 0, "size": 1, "filters": [{"field": "id", "operator": "=", "value": row_id}]}, self.module_config)
      debug_data.append({"sql": sql, "values": values})
      result = await conn.exec_driver_sql(sql, tuple(values))
      result = [dict(row._mapping) for row in result]

      self.convertor.decode_list(result)

      if len(result) > 0:
        return {"result": result[0]}
      else:
        return {"error": "update failed, query result is empty", }

    except SQLAlchemyError as err:
      return {"error": f"Error: {err}", }

  async def batch_update(self, conn: AsyncConnection, query_config, debug_data=[]):

    rows = GeneralUtils.get_value(query_config, 'rows', None)
    update_fields = GeneralUtils.get_value(query_config, 'updateFields', None)

    if rows is None or len(rows) == 0:
      return {"error": "rows parameter is missing", }
    self.convertor.encode_list(rows)

    row_id_list = []
    for row in rows:
      row_id = GeneralUtils.get_value(row, 'id', None)
      if row_id is None:
        return {
          "error": "row is missing field: id",
          "row": row,
        }
      row_id_list.append(row_id)

    try:
      for row in rows:
        sql, values = build_update_sql(self.module_config, row, update_fields)
        debug_data.append({"sql": sql, "values": values})
        await conn.exec_driver_sql(sql, tuple(values))

      await conn.commit()

      sql, values = build_query_sql({"all": True, "filters": [{"field": "id", "operator": "in", "value": row_id_list}]}, self.module_config)
      debug_data.append({"sql": sql, "values": values})
      result = await conn.exec_driver_sql(sql, tuple(values))
      result = [dict(row._mapping) for row in result]

      self.convertor.decode_list(result)

      if len(result) > 0:
        return {"result": result}
      else:
        return {"error": "update failed, query result is empty", }

    except SQLAlchemyError as err:
      return {"error": f"Error: {err}", }

  async def delete(self, conn: AsyncConnection, query_config, debug_data=[]):

    id = GeneralUtils.get_value(query_config, 'id', None)

    if id is None:
      return {"error": "id parameter is missing", }

    try:
      sql, values = build_delete_sql(self.module_config, id)
      debug_data.append({"sql": sql, "values": values})
      result = await conn.exec_driver_sql(sql, tuple(values))
      await conn.commit()

      deleted_rows = result.rowcount
      if deleted_rows >= 1:
        return {"deletedRows": deleted_rows}
      else:
        return {"error": f"delete failed, delete rows is {deleted_rows}", }
    except SQLAlchemyError as err:
      return {"error": f"Error: {err}", }


def get_default_orders(query_config, module_config):
  orders = GeneralUtils.get_value(query_config, 'orders', None)
  if orders is not None:
    return orders
  module_config_default_orders = GeneralUtils.get_value(
    GeneralUtils.get_value(module_config, 'default', {}),
    'orders',
    None
  )
  if module_config_default_orders is not None:
    return module_config_default_orders
  return {"field": "createdAt", "desc": True}

6.通用执行函数

新建文件“app/general/perform_general_operation.py”:

  • 这里我们定义一个通用的执行函数,可以通过字符串“type”参数来控制执行增删改查中的哪一个动作;
  • 参数conn为数据库连接对象,用来执行sql;
  • 参数module为模块的编码code,会通过这个module模块编码来查询数据库找到对应的模块配置信息;
  • 参数data为执行的参数,增删改查及其对应的批量操作,各自所需要的参数格式不同;
  • 参数debugData,中间执行的sql及其占位参数都会记录到这个debugData中;
  • 参数type,用来标识是增删改查中的哪一个操作;
import json
from typing import Literal

from sqlalchemy.ext.asyncio import AsyncConnection

from app.general.module_configs.default_module_configs import DefaultModuleConfigs
from app.general.sql_utils.GeneralService import GeneralService
from app.general.sql_utils.MODULE_CONFIG_MODULE import ModuleConfigsModule


async def perform_general_operation(
  conn: AsyncConnection,
  module: str,
  data: dict,
  debugData=None,
  type: Literal['list', 'item', 'insert', 'update', 'batchInsert', 'batchUpdate', 'delete'] = 'query'
):
  if debugData is None:
    debugData = []

  module_config = DefaultModuleConfigs.get(module, None)

  if module_config is None:

    module_query_result = await get_module_config(conn, module, debugData)

    if 'error' in module_query_result:
      return module_query_result

    module_config = module_query_result.get('result', {"": ""}).get('moduleConfig', None)

    if module_config is not None:
      module_config = json.loads(module_config)

  if module_config is None:
    return {
      "error": f"Can't find module config match: {module}",
      "body": module_query_result,
    }
  service = GeneralService(module_config=module_config)

  if type == 'list':
    return await service.select(conn, data, debugData)
  elif type == 'item':
    return await service.select_one(conn, data, debugData)
  elif type == 'insert':
    return await service.insert(conn, data, debugData)
  elif type == 'update':
    return await service.update(conn, data, debugData)
  elif type == 'batchInsert':
    return await service.batch_insert(conn, data, debugData)
  elif type == 'batchUpdate':
    return await service.batch_update(conn, data, debugData)
  elif type == 'delete':
    return await service.delete(conn, data, debugData)
  else:
    return {"error": f"Can't handle operation type: {type}"}


async def get_module_config(conn: AsyncConnection, module: str, debugData=None):
  service = GeneralService(module_config=ModuleConfigsModule)
  return await service.select_one(conn, {"code": module}, debugData)

7.动态路由注册

新建文件“app/general/add_generate_route.py”:

  • 这个文件暴露的函数“add_general_route”用来注册动态路由;
  • 除了注册标准的增删改查及其批量操作的路由接口之外,还注册了“/:module/remove”接口,作用等同于“/:module/delete”;
  • 还注册了post接口“/:module/batch”,作用等同于“/:module/batchInsert“;还注册了put接口“/:module/batch”,作用等同于“/:module/batchUpdate”;这两个接口的路由是相同的,但是post方法接口代表批量新建,put方法接口表示批量更新;
from fastapi import FastAPI
from starlette.responses import JSONResponse

from app.general.perform_general_operation import perform_general_operation
from app.utils.db_utils import AsyncSessionDep


def add_generate_route(
  app: FastAPI,
):
  types = ['list', 'item', 'insert', 'update', 'batchInsert', 'batchUpdate', 'delete']
  for type in types:
    @app.post("/{module}/" + type)
    async def func(module: str, query_param: dict, session: AsyncSessionDep, type=type):
      conn = await session.connection()
      try:
        result = await perform_general_operation(conn=conn, module=module, data=query_param, debugData=[], type=type)
        if "error" in result:
          return JSONResponse(content=result, status_code=500)
        else:
          return result
      finally:
        if conn:
          await conn.close()

  @app.post("/{module}/" + 'remove')
  async def func(module: str, query_param: dict, session: AsyncSessionDep, type='delete'):
    conn = await session.connection()
    try:
      result = await perform_general_operation(conn=conn, module=module, data=query_param, debugData=[], type=type)
      if "error" in result:
        return JSONResponse(content=result, status_code=500)
      else:
        return result
    finally:
      if conn:
        await conn.close()

  @app.post("/{module}/" + 'batch')
  async def func(module: str, query_param: dict, session: AsyncSessionDep, type='batchInsert'):
    conn = await session.connection()
    try:
      result = await perform_general_operation(conn=conn, module=module, data=query_param, debugData=[], type=type)
      if "error" in result:
        return JSONResponse(content=result, status_code=500)
      else:
        return result
    finally:
      if conn:
        await conn.close()

  @app.put("/{module}/" + 'batch')
  async def func(module: str, query_param: dict, session: AsyncSessionDep, type='batchUpdate'):
    conn = await session.connection()
    try:
      result = await perform_general_operation(conn=conn, module=module, data=query_param, debugData=[], type=type)
      if "error" in result:
        return JSONResponse(content=result, status_code=500)
      else:
        return result
    finally:
      if conn:
        await conn.close()

这里文件配置完毕之后,在FastAPI的入口文件不要忘了执行这个函数:

add_generate_route(app)

8.一些测试数据

8.1.DEMO_MODULE_CONFIG

DEMO_MODULE_CONFIG = {
  "tableName": "pl_demo",
  "base": "demo",
  "columns": {
    "id": {"valueType": "string"},
    "createdAt": {"valueType": "datetime"},
    "createdBy": {"valueType": "string"},
    "updatedAt": {"valueType": "datetime"},
    "updatedBy": {"valueType": "string"},
    "count": {"valueType": "number"},
    "normalText": {"valueType": "string"},
    "longText": {"valueType": "string"},
    "numberVal": {"valueType": "number"},
    "flag": {"valueType": "string"},
    "selectVal": {"valueType": "string"},
    "colorVal": {"valueType": "string"},
    "dateVal": {"valueType": "date"},
    "timeVal": {"valueType": "time"},
    "parentId": {"valueType": "string"},
    "imageId": {"valueType": "string"},
    "parentName": {"valueType": "string", "query": "t2.normal_text"},
    "provinceVal": {"valueType": "string"},
    "cityVal": {"valueType": "string"},
    "districtVal": {"valueType": "string"},
    "ovVal": {"valueType": "string"},
    "arrayJson": {"valueType": "string", "convert": "arrayjson"},
    "arrayString": {"valueType": "string", "convert": "arraystring"},
    "arrayJsonStr": {"valueType": "string"},
    "arrayStringStr": {"valueType": "string"}
  },
  "joinConfig": [
    {
      "type": "left join",
      "table": "pl_demo",
      "alia": "t2",
      "on": "t1.parent_id = t2.id"
    }
  ]
}

DEMO_LLM_USER_CONFIG = {
  "tableName": 'llm_user',
  "base": '/llm_user',
  "columns": {
    "id": {"valueType": "string"},
    "createdAt": {"valueType": "datetime"},
    "createdBy": {"valueType": "string"},
    "updatedAt": {"valueType": "datetime"},
    "updatedBy": {"valueType": "string"},
    "fullName": {"valueType": "string"},
    "username": {"valueType": "string"},
    "password": {"valueType": "number"},
    "memberStart": {"valueType": "string"},
    "memberEnd": {"valueType": "string"},
  },
}

8.2.DEMO_QUERY_CONFIG

DEMO_QUERY_ROWS = [
  {
    "id": "8570453e-0c88-11ef-9e34-5254001d6dcd",
    "createdAt": "2024-05-07 23:43:16",
    "createdBy": None,
    "updatedAt": "2024-10-31 14:41:28",
    "updatedBy": None,
    "count": 409,
    "normalText": "hello",
    "longText": None,
    "numberVal": 602,
    "flag": None,
    "selectVal": "consumer",
    "colorVal": None,
    "dateVal": "2024-05-14",
    "timeVal": None,
    "parentId": "9075e7c9-a9db-11ed-8add-525400138871",
    "imageId": None,
    "parentName": "Michael",
    "provinceVal": "310000",
    "cityVal": "310100",
    "districtVal": "310101",
    "ovVal": "member_discount",
    "arrayJson": [
      "consumer",
      "potential"
    ],
    "arrayString": [
      "consumer",
      "store"
    ],
    "arrayJsonStr": "[\"consumer\",\"potential\"]",
    "arrayStringStr": "consumer,potential"
  },
  {
    "id": "134437d3-fed2-11ee-9e34-5254001d6dcd",
    "createdAt": "2024-04-20 12:54:31",
    "createdBy": None,
    "updatedAt": "2024-10-31 14:10:46",
    "updatedBy": None,
    "count": 347,
    "normalText": "session",
    "longText": None,
    "numberVal": 456,
    "flag": None,
    "selectVal": "potential",
    "colorVal": None,
    "dateVal": "2024-04-03",
    "timeVal": None,
    "parentId": "90bf805f-a9db-11ed-8add-525400138871",
    "imageId": None,
    "parentName": "Michael3",
    "provinceVal": "150000",
    "cityVal": "150500",
    "districtVal": "150523",
    "ovVal": "member_discount",
    "arrayJson": [
      "consumer"
    ],
    "arrayString": [
      "consumer"
    ],
    "arrayJsonStr": "[\"consumer\",\"store\"]",
    "arrayStringStr": "consumer,store"
  }
]

DEMO_QUERY_CONFIG = {
  "offset": 0,
  "size": 8,
  "filters": [
    {
      "field": "normalText",
      "value": "ro",
      "operator": "~",
      "id": "query_meta_1"
    },
    {
      "field": "numberVal",
      "value": 100,
      "operator": ">",
    }
  ],
  "orders": [
    {
      "field": "createdAt",
      "desc": True
    },
    {
      "field": "updatedAt",
      "desc": False
    }
  ]
}

8.3.MODULE_CONFIG_MODULE

ModuleConfigsModule = {
  "tableName": 'pl_module',
  "base": '/module',
  "columns": {
    "id": {
      "valueType": 'string',
      "front": {
        "title": '编号',
      },
    },
    "createdAt": {
      "valueType": 'datetime',
      "front": {
        "title": '创建时间',
      },
    },
    "createdBy": {
      "valueType": 'string',
      "front": {
        "title": '创建人',
      },
    },
    "updatedAt": {
      "valueType": 'datetime',
      "front": {
        "title": '更新时间',
      },
    },
    "updatedBy": {
      "valueType": 'string',
      "front": {
        "title": '更新人',
      },
    },
    "label": {
      "valueType": 'string',
      "front": {
        "title": '模块名称',
      },
    },
    "code": {
      "valueType": 'string',
      "front": {
        "title": '模块标识',
      },
    },
    "remarks": {
      "valueType": 'string',
      "front": {
        "title": '备注',
      },
    },
    "moduleConfig": {
      "valueType": 'string',
      "front": {
        "title": '模块配置信息',
      },
    },
  },
}

9.一些预定义的模块

这里我们需要预定义一些模块配置,这些模块会经常需要执行查询动作,我们将其配置写死在系统中:

  • 选项值:ModuleConfigsOv;
  • 文件:ModuleConfigsFile;
  • 地址:ModuleConfigsAddress;

9.1.DefaultModuleConfigs

  • 新增文件“app/general/module_configs/default_module_configs.py”;
  • 这个文件中的变量DefaultModuleConfigs用来存放所有预定义的模块;
from app.general.module_configs.module_configs_address import ModuleConfigsAddress
from app.general.module_configs.module_configs_file import ModuleConfigsFile
from app.general.module_configs.module_configs_ov import ModuleConfigsOv
from app.general.sql_utils.MODULE_CONFIG_MODULE import ModuleConfigsModule

# 在查找moduleConfig的时候,优先从这个DefaultModuleConfigs中查找,减少请求数据库的次数
DefaultModuleConfigs = {
  "ov": ModuleConfigsOv,
  "file": ModuleConfigsFile,
  "upload": ModuleConfigsFile,
  "module": ModuleConfigsModule,
  "address": ModuleConfigsAddress,
}

9.2.ModuleConfigsAddress

ModuleConfigsAddress = {
  "tableName": 'pl_address',
  "base": '/address',
  "columns": {
    "id": {
      "valueType": 'string',
      "front": {
        "title": '编号',
      },
    },
    "createdAt": {
      "valueType": 'datetime',
      "front": {
        "title": '创建时间',
      },
    },
    "createdBy": {
      "valueType": 'string',
      "front": {
        "title": '创建人',
      },
    },
    "updatedAt": {
      "valueType": 'datetime',
      "front": {
        "title": '更新时间',
      },
    },
    "updatedBy": {
      "valueType": 'string',
      "front": {
        "title": '更新人',
      },
    },
    "name": {
      "valueType": 'string',
      "front": {
        "title": '地址名称',
      },
    },
    "code": {
      "valueType": 'string',
      "front": {
        "title": '地址编码',
      },
    },
    "deep": {
      "valueType": 'number',
      "front": {
        "title": '地址层级',
      },
    },
    "parentCode": {
      "valueType": 'string',
      "front": {
        "title": '父地址编码',
      },
    },
    "longitude": {
      "valueType": 'string',
      "front": {
        "title": '经度',
      },
    },
    "latitude": {
      "valueType": 'string',
      "front": {
        "title": '纬度',
      },
    },
    "parentName": {
      "front": {
        "title": '父地址名称',
      },
      "valueType": 'string',
      "query": 't2.name',
    },
  },
  "joinConfig": [
    {
      "type": 'left join',
      "table": 'pl_address',
      "alia": 't2',
      "on": 't1.parent_code = t2.code',
    },
  ],
}

9.3.ModuleConfigsFile

ModuleConfigsFile = {
  "tableName": 'pl_upload',
  "base": '/upload',
  "columns": {
    "id": {
      "valueType": 'string',
      "front": {
        "title": '编号',
      },
    },
    "createdAt": {
      "valueType": 'datetime',
      "front": {
        "title": '创建时间',
      },
    },
    "createdBy": {
      "valueType": 'string',
      "front": {
        "title": '创建人',
      },
    },
    "updatedAt": {
      "valueType": 'datetime',
      "front": {
        "title": '更新时间',
      },
    },
    "updatedBy": {
      "valueType": 'string',
      "front": {
        "title": '更新人',
      },
    },
    "name": {
      "valueType": 'string',
      "front": {
        "title": '文件名',
      },
    },
    "path": {
      "valueType": 'string',
      "front": {
        "title": '文件路径',
      },
    },
    "headId": {
      "valueType": 'string',
      "front": {
        "title": '文件所属对象ID',
      },
    },
    "parentId": {
      "valueType": 'string',
      "front": {
        "title": '文件的父ID',
      },
    },
    "attr1": {
      "valueType": 'string',
      "front": {
        "title": '扩展属性1',
      },
    },
    "attr2": {
      "valueType": 'string',
      "front": {
        "title": '扩展属性2',
      },
    },
    "attr3": {
      "valueType": 'string',
      "front": {
        "title": '扩展属性3',
      },
    },
  },
}

9.4.ModuleConfigsOv

ModuleConfigsOv = {
  "tableName": 'pl_option_value',
  "base": '/ov',
  "columns": {
    "id": {
      "valueType": 'string',
      "front": {
        "title": '编号',
      },
    },
    "createdAt": {
      "valueType": 'datetime',
      "front": {
        "title": '创建时间',
      },
    },
    "createdBy": {
      "valueType": 'string',
      "front": {
        "title": '创建人',
      },
    },
    "updatedAt": {
      "valueType": 'datetime',
      "front": {
        "title": '更新时间',
      },
    },
    "updatedBy": {
      "valueType": 'string',
      "front": {
        "title": '更新人',
      },
    },
    "name": {
      "valueType": 'string',
      "front": {
        "title": '显示值',
      },
    },
    "code": {
      "valueType": 'string',
      "front": {
        "title": '实际值',
      },
    },
    "type": {
      "valueType": 'string',
      "front": {
        "title": '所属类型',
      },
    },
    "comment": {
      "valueType": 'string',
      "front": {
        "title": '备注',
      },
    },
    "seq": {
      "valueType": 'number',
      "front": {
        "title": '顺序',
      },
    },
  },
}